// 导入Swing相关的类，用于创建图形用户界面（GUI），例如窗口、面板、按钮等组件，是Java中构建GUI应用程序常用的包
import javax.swing.*;
// 导入Java抽象窗口工具包（AWT）中的图形相关类，提供了如颜色、图形绘制、布局管理等功能，用于在GUI中进行图形绘制和界面布局等操作
import java.awt.*;
// 导入Java抽象窗口工具包（AWT）中的事件相关类，用于处理如鼠标点击、键盘按键等用户交互事件，使程序能够响应用户操作
import java.awt.event.*;
// 导入Java中用于生成随机数的类所在的包，在本程序中用于随机设置障碍物在窗口顶部出现的横坐标位置
import java.util.Random;

// GameFrame类继承自JFrame，作为游戏的主窗口框架0
public class GameFrame extends JFrame {
    GamePanel panel;

    // GameFrame类的构造函数，用于初始化游戏窗口相关设置
    public GameFrame() {
        // 创建一个GamePanel实例，用于承载游戏内容的绘制和交互逻辑
        panel = new GamePanel();
        // 设置当窗口关闭时，程序终止运行
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        // 将GamePanel添加到当前窗口中
        this.add(panel);
        // 根据组件的首选大小自动调整窗口大小
        this.pack();
        // 设置窗口在屏幕上居中显示
        this.setLocationRelativeTo(null);
        // 设置窗口可见，使窗口显示出来
        this.setVisible(true);
        // 为窗口添加键盘监听器，用于接收键盘输入事件，传入的是实现了KeyListener接口的类实例
        this.addKeyListener(new GameKeyListener(panel));
        // 确保GamePanel获取到键盘输入焦点，以便能够响应键盘事件
        panel.requestFocusInWindow();
    }
}

// GamePanel类继承自JPanel，用于绘制游戏画面以及处理游戏逻辑相关的更新等操作
class GamePanel extends JPanel {
    // 赛车在窗口中的初始横坐标位置
    int x = 150,
    // 赛车在窗口中的初始纵坐标位置
    y = 150;
    // 障碍物在窗口中的初始横坐标位置
    int obstacleX = 150,
    // 障碍物在窗口中的初始纵坐标位置（初始在窗口上方不可见位置）
    obstacleY = -100;
    // 障碍物的移动速度（每次更新移动的像素点数）
    int speed = 2;
    // 玩家的得分，初始为0
    int score = 0;

    // GamePanel类的构造函数，用于初始化-面板相关设置及启动游戏逻辑相关的定时器等
    public GamePanel() {
        // 设置面板的首选大小为宽400像素，高400像素
        this.setPreferredSize(new Dimension(400, 400));
        // 设置面板可以获取键盘焦点，这样才能响应键盘事件
        this.setFocusable(true);

        // 添加键盘监听器，通过匿名内部类的方式实现KeyAdapter，用于响应键盘按键按下事件
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                // 根据按下的不同方向键，改变赛车的坐标位置
                switch (e.getKeyCode()) {
                    // 如果按下向右方向键（VK_RIGHT），赛车横坐标增加10像素，向右移动
                    case KeyEvent.VK_RIGHT -> x += 10;
                    // 如果按下向左方向键（VK_LEFT），赛车横坐标减少10像素，向左移动
                    case KeyEvent.VK_LEFT -> x -= 10;
                    // 如果按下向上方向键（VK_UP），赛车纵坐标减少10像素，向上移动
                    case KeyEvent.VK_UP -> y -= 10;
                    // 如果按下向下方向键（VK_DOWN），赛车纵坐标增加10像素，向下移动
                    case KeyEvent.VK_DOWN -> y += 10;
                }
                // 调用repaint方法，触发面板重绘，以更新赛车的显示位置
                repaint();
            }
        });

        // 请求面板获取键盘输入焦点，确保能接收到键盘事件
        this.requestFocusInWindow();
        // 创建一个定时器，每隔20毫秒触发一次指定的动作（传入ActionListener的实现），这里用于定时更新游戏状态
        Timer timer = new Timer(20, e -> gameUpdate());
        // 启动定时器，开始定时执行任务
        timer.start();
    }

    // 游戏状态更新的方法，用于更新游戏中各个元素的状态（如障碍物的位置等）
    private void gameUpdate() {
        // 让障碍物的纵坐标增加，实现向下移动的效果（根据设定的速度）
        obstacleY += speed;
        // 如果障碍物已经移出面板底部（纵坐标大于面板高度），说明已经越过屏幕，需要重新初始化其位置
        if (obstacleY > this.getHeight()) {
            // 将障碍物的纵坐标重置到窗口上方不可见位置
            obstacleY = -100;
            // 在窗口宽度范围内随机设置障碍物的横坐标位置（减去30是为了保证障碍物完全在窗口内）
            obstacleX = new Random().nextInt(this.getWidth() - 30);
            // 每次成功避开一个障碍物，玩家得分加1
            score++;
        }
        // 检查赛车与障碍物是否发生碰撞
        checkCollision();
        // 调用repaint方法，触发面板重绘，更新游戏画面显示
        repaint();
    }

    // 检查赛车与障碍物是否发生碰撞的方法
    private void checkCollision() {
        // 根据赛车当前的位置和大小创建一个矩形对象，用于表示赛车的碰撞区域
        Rectangle carRect = new Rectangle(x, y, 50, 30);
        // 根据障碍物当前的位置和大小创建一个矩形对象，用于表示障碍物的碰撞区域
        Rectangle obstacleRect = new Rectangle(obstacleX, obstacleY, 30, 30);
        // 判断两个矩形是否相交，即赛车与障碍物是否发生碰撞
        if (carRect.intersects(obstacleRect)) {
            // 如果发生碰撞，弹出一个消息对话框显示游戏结束以及玩家的得分情况
            JOptionPane.showMessageDialog(this, "游戏结束！你的得分是：" + score);
            // 直接退出整个程序（也可以选择重置游戏状态来继续游戏，而不是直接退出）
            System.exit(0);
        }
    }



    // 重写paintComponent方法，用于绘制游戏画面中的各个元素（背景、赛车、障碍物、得分显示等）
    @Override
    protected void paintComponent(Graphics g) {
        // 调用父类的paintComponent方法，先进行默认的绘制操作（如清除之前的画面等）
        super.paintComponent(g);
        // 设置绘制颜色为浅灰色，用于绘制游戏背景
        g.setColor(Color.LIGHT_GRAY);
        // 填充整个面板区域，绘制出背景
        g.fillRect(0, 0, this.getWidth(), this.getHeight());
        // 设置绘制颜色为红色，用于绘制赛车
        g.setColor(Color.RED);
        // 根据赛车当前的坐标位置和大小绘制赛车（用矩形表示）
        g.fillRect(x, y, 50, 30);

        // 设置绘制颜色为黑色，用于绘制障碍物
        g.setColor(Color.BLACK);
        // 根据障碍物当前的坐标位置和大小绘制障碍物（用矩形表示）
        g.fillRect(obstacleX, obstacleY, 30, 30);

        // 设置绘制颜色为黑色，用于绘制得分显示文本
        g.setColor(Color.BLACK);
        // 在指定位置（横坐标10，纵坐标20）绘制得分显示文本，格式为"得分：具体分数"
        g.drawString("得分：" + score, 10, 20);
    }

    // 用于移动赛车的方法，根据传入的横纵坐标偏移量来改变赛车的位置，并触发重绘更新画面
    public void moveCar(int dx, int dy) {
        x += dx;
        y += dy;
        this.repaint();
    }
}

// GameKeyListener类实现了KeyListener接口，用于处理键盘事件并将其转换为对GamePanel中赛车移动的操作
class GameKeyListener implements KeyListener {
    GamePanel panel;

    // GameKeyListener类的构造函数，传入GamePanel实例，以便后续操作对应的面板
    public GameKeyListener(GamePanel panel) {
        this.panel = panel;
    }

    // 由于一般情况下，keyTyped方法在游戏中不太常用，这里故意留空，只是为了满足KeyListener接口的实现要求
    @Override
    public void keyTyped(KeyEvent e) {
    }

    // 处理键盘按键按下事件的方法，根据按下的不同按键，调用GamePanel的moveCar方法来移动赛车
    @Override
// 重写（Override）接口中的方法，这里重写的是KeyListener接口中的keyPressed方法，表明这是对键盘按键按下事件的具体处理逻辑实现
    public void keyPressed(KeyEvent e) {
        // 初始化赛车在横坐标方向上的移动偏移量为0，后续会根据按下的不同方向键来改变该值，正值表示向右移动，负值表示向左移动
        int dx = 0;
        // 初始化赛车在纵坐标方向上的移动偏移量为0，后续会根据按下的不同方向键来改变该值，正值表示向下移动，负值表示向上移动
        int dy = 0;

        // 通过获取按下键盘按键对应的键码（KeyCode）来判断具体按下了哪个方向键，进而决定赛车如何移动
        switch (e.getKeyCode()) {·
            // 如果按下的是向右方向键（在KeyEvent中用VK_RIGHT表示）
            case KeyEvent.VK_RIGHT -> dx = 10;
            // 那么将横坐标方向的移动偏移量dx设置为10，表示赛车在横坐标方向（向右）移动10个像素

            // 如果按下的是向左方向键（在KeyEvent中用VK_LEFT表示）
            case KeyEvent.VK_LEFT -> dx = -10;
            // 那么将横坐标方向的移动偏移量dx设置为 -10，表示赛车在横坐标方向（向左）移动10个像素

            // 如果按下的是向上方向键（在KeyEvent中用VK_UP表示）
            case KeyEvent.VK_UP -> dy = -10;
            // 那么将纵坐标方向的移动偏移量dy设置为 -10，表示赛车在纵坐标方向（向上）移动10个像素

            // 如果按下的是向下方向键（在KeyEvent中用VK_DOWN表示）
            case KeyEvent.VK_DOWN -> dy = 10;
            // 那么将纵坐标方向的移动偏移量dy设置为10，表示赛车在纵坐标方向（向下）移动10个像素
        }

        // 调用GamePanel类中的moveCar方法，传入计算好的横坐标和纵坐标方向的移动偏移量dx和dy，0000
        // 以此来实现根据用户按下的方向键移动赛车在游戏面板中的位置
        panel.moveCar(dx, dy);
    }

    // 由于一般情况下，keyReleased方法在游戏中不太常用，这里故意留空，只是为了满足KeyListener接口的实现要求
    @Override
    public void keyReleased(KeyEvent e) {
    }
}